<?php

namespace Aws\S3\Crypto;

use Aws\Crypto\DecryptionTrait;
use Aws\HashingStream;
use Aws\PhpHash;
use Aws\Crypto\AbstractCryptoClient;
use Aws\Crypto\EncryptionTrait;
use Aws\Crypto\MetadataEnvelope;
use Aws\Crypto\MaterialsProvider;
use Aws\Crypto\Cipher\CipherBuilderTrait;
use Aws\S3\S3Client;
use GuzzleHttp\Promise;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7;

/**
 * Provides a wrapper for an S3Client that supplies functionality to encrypt
 * data on putObject[Async] calls and decrypt data on getObject[Async] calls.
 *
 * Legacy implementation using older encryption workflow.
 *
 * AWS strongly recommends the upgrade to the S3EncryptionClientV2 (over the
 * S3EncryptionClient), as it offers updated data security best practices to our
 * customers who upgrade. S3EncryptionClientV2 contains breaking changes, so this
 * will require planning by engineering teams to migrate. New workflows should
 * just start with S3EncryptionClientV2.
 *
 * @deprecated
 */
class S3EncryptionClient extends AbstractCryptoClient {
	use CipherBuilderTrait;
	use CryptoParamsTrait;
	use DecryptionTrait;
	use EncryptionTrait;
	use UserAgentTrait;

	const CRYPTO_VERSION = '1n';

	private $client;
	private $instructionFileSuffix;

	/**
	 * @param S3Client $client The S3Client to be used for true uploading and
	 *                         retrieving objects from S3 when using the
	 *                         encryption client.
	 * @param string|null $instructionFileSuffix Suffix for a client wide
	 *                                           default when using instruction
	 *                                           files for metadata storage.
	 */
	public function __construct(
		S3Client $client,
		         $instructionFileSuffix = null
	) {
		$this->appendUserAgent($client, 'feat/s3-encrypt/'.self::CRYPTO_VERSION);
		$this->client = $client;
		$this->instructionFileSuffix = $instructionFileSuffix;
	}

	private static function getDefaultStrategy() {
		return new HeadersMetadataStrategy();
	}

	/**
	 * Encrypts the data in the 'Body' field of $args and promises to upload it
	 * to the specified location on S3.
	 *
	 * @param array $args Arguments for encrypting an object and uploading it
	 *                    to S3 via PutObject.
	 *
	 * The required configuration arguments are as follows:
	 *
	 * - @MaterialsProvider: (MaterialsProvider) Provides Cek, Iv, and Cek
	 *   encrypting/decrypting for encryption metadata.
	 * - @CipherOptions: (array) Cipher options for encrypting data. Only the
	 *   Cipher option is required. Accepts the following:
	 *       - Cipher: (string) cbc|gcm
	 *            See also: AbstractCryptoClient::$supportedCiphers. Note that
	 *            cbc is deprecated and gcm should be used when possible.
	 *       - KeySize: (int) 128|192|256
	 *            See also: MaterialsProvider::$supportedKeySizes
	 *       - Aad: (string) Additional authentication data. This option is
	 *            passed directly to OpenSSL when using gcm. It is ignored when
	 *            using cbc. Note if you pass in Aad for gcm encryption, the
	 *            PHP SDK will be able to decrypt the resulting object, but other
	 *            AWS SDKs may not be able to do so.
	 *
	 * The optional configuration arguments are as follows:
	 *
	 * - @MetadataStrategy: (MetadataStrategy|string|null) Strategy for storing
	 *   MetadataEnvelope information. Defaults to using a
	 *   HeadersMetadataStrategy. Can either be a class implementing
	 *   MetadataStrategy, a class name of a predefined strategy, or empty/null
	 *   to default.
	 * - @InstructionFileSuffix: (string|null) Suffix used when writing to an
	 *   instruction file if using an InstructionFileMetadataHandler.
	 *
	 * @return PromiseInterface
	 *
	 * @throws \InvalidArgumentException Thrown when arguments above are not
	 *                                   passed or are passed incorrectly.
	 */
	public function putObjectAsync(array $args) {
		$provider = $this->getMaterialsProvider($args);
		unset($args['@MaterialsProvider']);

		$instructionFileSuffix = $this->getInstructionFileSuffix($args);
		unset($args['@InstructionFileSuffix']);

		$strategy = $this->getMetadataStrategy($args, $instructionFileSuffix);
		unset($args['@MetadataStrategy']);

		$envelope = new MetadataEnvelope();

		return Promise\Create::promiseFor($this->encrypt(
			Psr7\Utils::streamFor($args['Body']),
			$args['@CipherOptions'] ?: [],
			$provider,
			$envelope
		))->then(
			function($encryptedBodyStream) use ($args) {
				$hash = new PhpHash('sha256');
				$hashingEncryptedBodyStream = new HashingStream(
					$encryptedBodyStream,
					$hash,
					self::getContentShaDecorator($args)
				);
				return [$hashingEncryptedBodyStream, $args];
			}
		)->then(
			function($putObjectContents) use ($strategy, $envelope) {
				list($bodyStream, $args) = $putObjectContents;
				if($strategy === null) {
					$strategy = self::getDefaultStrategy();
				}

				$updatedArgs = $strategy->save($envelope, $args);
				$updatedArgs['Body'] = $bodyStream;
				return $updatedArgs;
			}
		)->then(
			function($args) {
				unset($args['@CipherOptions']);
				return $this->client->putObjectAsync($args);
			}
		);
	}

	private static function getContentShaDecorator(&$args) {
		return function($hash) use (&$args) {
			$args['ContentSHA256'] = bin2hex($hash);
		};
	}

	/**
	 * Encrypts the data in the 'Body' field of $args and uploads it to the
	 * specified location on S3.
	 *
	 * @param array $args Arguments for encrypting an object and uploading it
	 *                    to S3 via PutObject.
	 *
	 * The required configuration arguments are as follows:
	 *
	 * - @MaterialsProvider: (MaterialsProvider) Provides Cek, Iv, and Cek
	 *   encrypting/decrypting for encryption metadata.
	 * - @CipherOptions: (array) Cipher options for encrypting data. A Cipher
	 *   is required. Accepts the following options:
	 *       - Cipher: (string) cbc|gcm
	 *            See also: AbstractCryptoClient::$supportedCiphers. Note that
	 *            cbc is deprecated and gcm should be used when possible.
	 *       - KeySize: (int) 128|192|256
	 *            See also: MaterialsProvider::$supportedKeySizes
	 *       - Aad: (string) Additional authentication data. This option is
	 *            passed directly to OpenSSL when using gcm. It is ignored when
	 *            using cbc. Note if you pass in Aad for gcm encryption, the
	 *            PHP SDK will be able to decrypt the resulting object, but other
	 *            AWS SDKs may not be able to do so.
	 *
	 * The optional configuration arguments are as follows:
	 *
	 * - @MetadataStrategy: (MetadataStrategy|string|null) Strategy for storing
	 *   MetadataEnvelope information. Defaults to using a
	 *   HeadersMetadataStrategy. Can either be a class implementing
	 *   MetadataStrategy, a class name of a predefined strategy, or empty/null
	 *   to default.
	 * - @InstructionFileSuffix: (string|null) Suffix used when writing to an
	 *   instruction file if an using an InstructionFileMetadataHandler was
	 *   determined.
	 *
	 * @return \Aws\Result PutObject call result with the details of uploading
	 *                     the encrypted file.
	 *
	 * @throws \InvalidArgumentException Thrown when arguments above are not
	 *                                   passed or are passed incorrectly.
	 */
	public function putObject(array $args) {
		return $this->putObjectAsync($args)->wait();
	}

	/**
	 * Promises to retrieve an object from S3 and decrypt the data in the
	 * 'Body' field.
	 *
	 * @param array $args Arguments for retrieving an object from S3 via
	 *                    GetObject and decrypting it.
	 *
	 * The required configuration argument is as follows:
	 *
	 * - @MaterialsProvider: (MaterialsProvider) Provides Cek, Iv, and Cek
	 *   encrypting/decrypting for decryption metadata. May have data loaded
	 *   from the MetadataEnvelope upon decryption.
	 *
	 * The optional configuration arguments are as follows:
	 *
	 * - SaveAs: (string) The path to a file on disk to save the decrypted
	 *   object data. This will be handled by file_put_contents instead of the
	 *   Guzzle sink.
	 *
	 * - @MetadataStrategy: (MetadataStrategy|string|null) Strategy for reading
	 *   MetadataEnvelope information. Defaults to determining based on object
	 *   response headers. Can either be a class implementing MetadataStrategy,
	 *   a class name of a predefined strategy, or empty/null to default.
	 * - @InstructionFileSuffix: (string) Suffix used when looking for an
	 *   instruction file if an InstructionFileMetadataHandler is being used.
	 * - @CipherOptions: (array) Cipher options for decrypting data. A Cipher
	 *   is required. Accepts the following options:
	 *       - Aad: (string) Additional authentication data. This option is
	 *            passed directly to OpenSSL when using gcm. It is ignored when
	 *            using cbc.
	 *
	 * @return PromiseInterface
	 *
	 * @throws \InvalidArgumentException Thrown when required arguments are not
	 *                                   passed or are passed incorrectly.
	 */
	public function getObjectAsync(array $args) {
		$provider = $this->getMaterialsProvider($args);
		unset($args['@MaterialsProvider']);

		$instructionFileSuffix = $this->getInstructionFileSuffix($args);
		unset($args['@InstructionFileSuffix']);

		$strategy = $this->getMetadataStrategy($args, $instructionFileSuffix);
		unset($args['@MetadataStrategy']);

		$saveAs = null;
		if(!empty($args['SaveAs'])) {
			$saveAs = $args['SaveAs'];
		}

		$promise = $this->client->getObjectAsync($args)
			->then(
				function($result) use (
					$provider,
					$instructionFileSuffix,
					$strategy,
					$args
				) {
					if($strategy === null) {
						$strategy = $this->determineGetObjectStrategy(
							$result,
							$instructionFileSuffix
						);
					}

					$envelope = $strategy->load($args + [
							'Metadata' => $result['Metadata']
						]);

					$provider = $provider->fromDecryptionEnvelope($envelope);

					$result['Body'] = $this->decrypt(
						$result['Body'],
						$provider,
						$envelope,
						isset($args['@CipherOptions'])
							? $args['@CipherOptions']
							: []
					);
					return $result;
				}
			)->then(
				function($result) use ($saveAs) {
					if(!empty($saveAs)) {
						file_put_contents(
							$saveAs,
							(string)$result['Body'],
							LOCK_EX
						);
					}
					return $result;
				}
			);

		return $promise;
	}

	/**
	 * Retrieves an object from S3 and decrypts the data in the 'Body' field.
	 *
	 * @param array $args Arguments for retrieving an object from S3 via
	 *                    GetObject and decrypting it.
	 *
	 * The required configuration argument is as follows:
	 *
	 * - @MaterialsProvider: (MaterialsProvider) Provides Cek, Iv, and Cek
	 *   encrypting/decrypting for decryption metadata. May have data loaded
	 *   from the MetadataEnvelope upon decryption.
	 *
	 * The optional configuration arguments are as follows:
	 *
	 * - SaveAs: (string) The path to a file on disk to save the decrypted
	 *   object data. This will be handled by file_put_contents instead of the
	 *   Guzzle sink.
	 * - @InstructionFileSuffix: (string|null) Suffix used when looking for an
	 *   instruction file if an InstructionFileMetadataHandler was detected.
	 * - @CipherOptions: (array) Cipher options for encrypting data. A Cipher
	 *   is required. Accepts the following options:
	 *       - Aad: (string) Additional authentication data. This option is
	 *            passed directly to OpenSSL when using gcm. It is ignored when
	 *            using cbc.
	 *
	 * @return \Aws\Result GetObject call result with the 'Body' field
	 *                     wrapped in a decryption stream with its metadata
	 *                     information.
	 *
	 * @throws \InvalidArgumentException Thrown when arguments above are not
	 *                                   passed or are passed incorrectly.
	 */
	public function getObject(array $args) {
		return $this->getObjectAsync($args)->wait();
	}
}
